package opa

import (
	"context"
	"fmt"
	"github.com/nuttech/bell"
	"github.com/open-policy-agent/opa/ast"
	"github.com/open-policy-agent/opa/rego"
	"github.com/pkg/errors"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/logs"
	"gitverse.ru/synapse/kubelatte/pkg/util"
	"sort"
	"sync"
)

const (
	RegoUpdateTemplate = "RegoUpdateTemplate"
	RegoDeleteTemplate = "RegoDeleteTemplate"
)

var OpaController = OpaManager{}

type OpaManager struct {
	rwMutex    sync.Mutex
	Rules      map[string]rego.PreparedEvalQuery
	senderMode bool
}

func (m *OpaManager) Start(senderMode bool) {
	logs.Debugf("[OpaManager] start")
	m.Rules = map[string]rego.PreparedEvalQuery{}
	bell.Listen(RegoUpdateTemplate, m.AddNewObject)
	bell.Listen(RegoDeleteTemplate, m.RemoveObject)
	OpaController.senderMode = senderMode
}

func (m *OpaManager) AddNewObject(message bell.Message) {
	logs.Debugf("[OpaManager] receive new event AddNewObject")
	go m.ProcessRule(message)
}

func (m *OpaManager) ProcessRule(message bell.Message) {
	var err error
	defer m.rwMutex.Unlock()
	m.rwMutex.Lock()
	t := message.Value.(v1alpha1.Template)
	logs.Debugf("[OpaManager] register template %s", t.GetNamespacedName())

	peq, err := m.Precompile(t.Spec.Data)
	if err != nil {
		logs.Errorf("Precompile error: %s", err)
		return
	}
	m.Rules[t.GetNamespacedName()] = peq
	if m.senderMode {
		util.SendEvent("SyncAddNewObjectEvent", map[string]interface{}{
			"type": "update",
			"obj":  t,
		})
	}
}

func CapabilitiesForThisVersion() *ast.Capabilities {
	f := &ast.Capabilities{}

	// f.Builtins contains 149 allowed built-in functions
	f.Builtins = []*ast.Builtin{
		// Unification/equality ("=")
		ast.Equality,

		// Assignment (":=")
		ast.Assign,

		// Membership, infix "in": x in xs
		ast.Member,
		ast.MemberWithKey,

		// Comparisons
		ast.GreaterThan,
		ast.GreaterThanEq,
		ast.LessThan,
		ast.LessThanEq,
		ast.NotEqual,
		ast.Equal,

		// Arithmetic
		ast.Plus,
		ast.Minus,
		ast.Multiply,
		ast.Divide,
		ast.Ceil,
		ast.Floor,
		ast.Round,
		ast.Abs,
		ast.Rem,

		// Bitwise Arithmetic
		ast.BitsOr,
		ast.BitsAnd,
		ast.BitsNegate,
		ast.BitsXOr,
		ast.BitsShiftLeft,
		ast.BitsShiftRight,

		// Binary
		ast.And,
		ast.Or,

		// Aggregates
		ast.Count,
		ast.Sum,
		ast.Product,
		ast.Max,
		ast.Min,
		ast.Any,
		ast.All,

		// Arrays
		ast.ArrayConcat,
		ast.ArraySlice,
		ast.ArrayReverse,

		// Conversions
		ast.ToNumber,

		// Regular Expressions
		ast.RegexIsValid,
		ast.RegexMatch,
		ast.RegexMatchDeprecated,
		ast.RegexSplit,
		ast.GlobsMatch,
		ast.RegexTemplateMatch,
		ast.RegexFind,
		ast.RegexFindAllStringSubmatch,
		ast.RegexReplace,

		// Sets
		ast.SetDiff,
		ast.Intersection,
		ast.Union,

		// Strings
		ast.AnyPrefixMatch,
		ast.AnySuffixMatch,
		ast.Concat,
		ast.FormatInt,
		ast.IndexOf,
		ast.IndexOfN,
		ast.Substring,
		ast.Lower,
		ast.Upper,
		ast.Contains,
		ast.StartsWith,
		ast.EndsWith,
		ast.Split,
		ast.Replace,
		ast.ReplaceN,
		ast.Trim,
		ast.TrimLeft,
		ast.TrimPrefix,
		ast.TrimRight,
		ast.TrimSuffix,
		ast.TrimSpace,
		ast.Sprintf,
		ast.StringReverse,

		// Numbers
		ast.NumbersRange,

		// Encoding
		ast.JSONMarshal,
		ast.JSONUnmarshal,
		ast.JSONIsValid,
		ast.Base64Encode,
		ast.Base64Decode,
		ast.Base64IsValid,
		ast.Base64UrlEncode,
		ast.Base64UrlEncodeNoPad,
		ast.Base64UrlDecode,
		ast.URLQueryDecode,
		ast.URLQueryEncode,
		ast.URLQueryEncodeObject,
		ast.URLQueryDecodeObject,
		ast.YAMLMarshal,
		ast.YAMLUnmarshal,
		ast.YAMLIsValid,
		ast.HexEncode,
		ast.HexDecode,

		// Object Manipulation
		ast.ObjectUnion,
		ast.ObjectUnionN,
		ast.ObjectRemove,
		ast.ObjectFilter,
		ast.ObjectGet,
		ast.ObjectKeys,
		ast.ObjectSubset,

		// JSON Object Manipulation
		ast.JSONFilter,
		ast.JSONRemove,
		ast.JSONPatch,

		// Time
		ast.ParseNanos,
		ast.ParseRFC3339Nanos,
		ast.ParseDurationNanos,
		ast.Format,
		ast.Date,
		ast.Clock,
		ast.Weekday,
		ast.AddDate,
		ast.Diff,

		// Crypto
		//ast.CryptoX509ParseCertificates,
		//ast.CryptoX509ParseAndVerifyCertificates,
		//ast.CryptoMd5,
		//ast.CryptoSha1,
		ast.CryptoSha256,
		//ast.CryptoX509ParseCertificateRequest,
		//ast.CryptoX509ParseRSAPrivateKey,
		//ast.CryptoX509ParseKeyPair,
		//ast.CryptoParsePrivateKeys,
		//ast.CryptoHmacMd5,
		//ast.CryptoHmacSha1,
		//ast.CryptoHmacSha256,
		//ast.CryptoHmacSha512,
		//ast.CryptoHmacEqual,

		// Sort
		ast.Sort,

		// Types
		ast.IsNumber,
		ast.IsString,
		ast.IsBoolean,
		ast.IsArray,
		ast.IsSet,
		ast.IsObject,
		ast.IsNull,
		ast.TypeNameBuiltin,

		// JSON Schema
		ast.JSONSchemaVerify,
		ast.JSONMatchSchema,

		// Glob
		ast.GlobMatch,
		ast.GlobQuoteMeta,

		// Units
		ast.UnitsParse,
		ast.UnitsParseBytes,

		// SemVers
		ast.SemVerIsValid,
		ast.SemVerCompare,
	}
	sort.Slice(f.Builtins, func(i, j int) bool {
		return f.Builtins[i].Name < f.Builtins[j].Name
	})
	return f
}

func (m *OpaManager) Precompile(module string) (rego.PreparedEvalQuery, error) {
	ctx := context.Background()

	template := "package kubelatte.rego\n" + module

	query, err := rego.New(
		rego.Query("x = data.kubelatte.rego.violation"),
		rego.Module("kubelatte.rego", template),
		rego.Capabilities(CapabilitiesForThisVersion()),
	).PrepareForEval(ctx)

	if err != nil {
		logs.Errorf("[OpaManager] Precompile failed %s", err)

		return rego.PreparedEvalQuery{}, errors.Wrap(err, "[OpaManager] Precompile failed %s")
	}
	return query, nil
}

// CheckRule return result, detailedMessages
func (m *OpaManager) CheckRule(name string, parameters, inputObject map[string]interface{}) (bool, []string) {
	ctx := context.Background()
	if rule, ok := m.Rules[name]; ok {
		input := map[string]interface{}{
			"review":     inputObject,
			"parameters": parameters,
		}

		results, err := rule.Eval(ctx, rego.EvalInput(input))
		var sErr = ""
		if err != nil {
			// Handle evaluation error.
			sErr = fmt.Sprintf("[OpaManager] CheckRule %s failed %s", name, err)
			logs.Errorf(sErr)
		} else if len(results) == 0 {
			// Handle undefined result.
			sErr = fmt.Sprintf("[OpaManager] CheckRule %s failed, result zero. (exec failed?)", name)
			logs.Errorf(sErr)
		} else if result, ok := results[0].Bindings["x"].([]interface{}); !ok {
			sErr = fmt.Sprintf("[OpaManager] CheckRule %s failed, result exist, but unexpected. (exec failed?)", name)
			logs.Errorf(sErr)
		} else {
			// Handle result/decision.
			logs.Debugf("Rule done, result: %v", result)
			if len(result) > 0 {
				var details []string

				for _, r := range result {
					details = append(details, r.(map[string]interface{})["msg"].(string))
				}

				return false, details
			} else {
				return true, nil
			}
		}

	}

	return false, []string{fmt.Sprintf("Rule %s not found", name)}
}

func (m *OpaManager) RemoveObject(message bell.Message) {
	m.rwMutex.Lock()
	t := message.Value.(v1alpha1.Template)
	delete(m.Rules, t.GetNamespacedName())
	m.rwMutex.Unlock()
	if m.senderMode {
		util.SendEvent("SyncAddNewObjectEvent", map[string]interface{}{
			"type": "remove",
			"obj":  t,
		})
	}
}

func (m *OpaManager) GetRegoTemplates() map[string]rego.PreparedEvalQuery {
	return m.Rules
}
